{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 从零手搓中文大模型｜🚀 Day09"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "微软的`Tinystories`论文里，是直接在200w条`Instruction`数据上做的全量`pretrain`来验证小参数`LLM`的指令遵从效果的。\n",
    "\n",
    "为了挖掘`SLM`的潜力，我想看看在超小规模参数的情况下，少量（相比于`pretrain`）数据的`SFT`是否能起作用。\n",
    "\n",
    "（当然还有一个原因是要把`Instruction`数据全通过`GPT API`翻译一遍还是相当贵的😂）。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 全参数SFT训练实验🧪"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "上一期分享了一些`SFT`训练相关的知识点，里面提到了关于训练模式的选择。\n",
    "\n",
    "我的这个项目里，用于`SFT`训练的数据和之前预训练的数据分布是非常相似的，所以这里不打算将`SFT`数据用于`continue pretrain`，而是直接将`SFT`数据用于`finetuning`。\n",
    "\n",
    "由于`SFT`全量`finetuning`其实本质上和`pretrain`没有什么差别，只是在计算`loss`的时候对`prompt`部分做了一个`mask`，所以这里就不对训练参数配置做过多的介绍了。\n",
    "\n",
    "> 这里额外提一点，我在上构造的数据基础上做了一个增强的操作（用`GPT API`翻译还是太贵了😂）。\n",
    "> \n",
    "> 具体操作是：将上期用吴恩达老师的`translation-agent`翻译构造的数据里的指令部分里的多个约束抽取成了`key: value`，然后随机排列，输出还是故事本身不变，这样就得到了很多新的数据（从之前的1.3w条数据增加到了7.1w条）。\n",
    "> \n",
    "> 另外还有一个潜在的好处就是可以让模型知道指令内部的多个约束的顺序是不敏感的，只要输出符合所有指令的约束就可以。\n",
    "\n",
    "我简单地做了几组实验：\n",
    "\n",
    "🟣 `learning_rate = 1e-4, bf16-true`\n",
    "\n",
    "🔴 `learning_rate = 5e-4, bf16-true`\n",
    "\n",
    "🟢 `learning_rate = 5e-4, bf16-true`，但学习率下降比前两者速度更快\n",
    "\n",
    "🔵 `learning_rate = 5e-4, bf16-mixed`，学习率和上一个一样\n",
    "\n",
    "![image_v2](https://erxuanyi-1257355350.cos.ap-beijing.myqcloud.com/image_v2.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> 为了方便观察，图里的曲线都是经过平滑之后的。\n",
    "\n",
    "可以发现几个问题🤔：\n",
    "\n",
    "1. 学习率使用`pretrain`的1/5的时候（`1e-4`），收敛程度不如使用和`pretrain`时一样的`5e-4`。\n",
    "\n",
    "   和上一期里搜集的经验描述有些不一致（`SFT`阶段的`learning_rate`使用`pretrain`的1/10的建议）。\n",
    "   \n",
    "   我个人理解是因为我的`SFT`数据和`pretrain`数据非常相似，且指令相对简单/单一（只是在故事前面加了一些约束文本），所以即使用比较大的学习率也没有出现震荡发散的情况，反而很容易收敛。\n",
    "\n",
    "2. 学习率被设置得下降更快的这一组，收敛速度也更快一些，这个也容易理解：在后期，模型已经非常接近最优解了，这时候学习率下降得快，可以更精细地学习以逼近最优解。\n",
    "3. 使用`bf16-mixed`的这一组，收敛速度和前一个差不多，但是loss整体还要更低一些"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 结果测试\n",
    "#### 单一约束\n",
    "\n",
    "我随便构造了几个简单的测试用例，其中的指令都只包含单一的约束。\n",
    "\n",
    "结果如下👇："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 60,
   "metadata": {},
   "outputs": [],
   "source": [
    "from litgpt import LLM\n",
    "from litgpt.prompts import Phi2\n",
    "import json\n",
    "\n",
    "sft_data = json.load(\n",
    "    open(\n",
    "        \"../../Data/TinyStoriesInstruct/sft_data_v2_simple.json\", \"r\", encoding=\"utf-8\"\n",
    "    )\n",
    ")\n",
    "\n",
    "prompt_style = Phi2()\n",
    "llm = LLM.load(\n",
    "    model=\"../../Experiments/Output/sft/microstories_v2/bf16_true_1e-4/step-000600\"\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 61,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "随机句子：他们证明了即使是最小的朋友，团结起来也能变得很强大。\n",
      "摘要：一辆大卡车被困在事故现场，一只小鸟和它的朋友们前来救援，帮助搬开残骸，让卡车继续前行。他们成为朋友，并继续互相帮助。\n",
      "特征：对话\n",
      "词汇：醒来，事故现场，强大\n",
      "----------------------------------------------------------------------------------------------------\n",
      "一天，一辆强壮的大卡车正在路上行驶。卡车非常大且强壮。它可以载很多东西。卡车司机很开心，因为他的工作很有趣。  \n",
      "突然，卡车撞上了一个大事故现场。事故现场一片混乱。卡车司机很伤心，因为他再也开不了卡车了。他说：“哦不！我的卡车被卡住了！我该怎么办？”  \n",
      "一只小鸟听到了卡车司机的话。小鸟说：“别担心，我可以帮你！”小鸟飞走了，带着它的朋友们回来。他们齐心协力搬开事故现场。  \n",
      "卡车司机非常高兴。他说：“谢谢你们，小鸟们！你们救了我的卡车！”小鸟们说：“不客气！我们很高兴能帮上忙！”于是他们都成了好朋友。  \n",
      "从那天起，卡车司机和小鸟们就开始互相帮助了。他们展示了即使是最小的朋友在一起合作时也能变得强大。每天早晨，鸟儿们都会用快乐的歌声叫醒卡车司机。\n",
      "----------------------------------------------------------------------------------------------------\n",
      "一天，一辆强壮的大卡车正在路上行驶。卡车非常大且强壮。它可以载很多东西。卡车司机很开心，因为他的工作很有趣。  \n",
      "突然，卡车撞上了一个大事故现场。事故现场一片混乱。卡车司机很伤心，因为他再也开不了卡车了。他说：“哦不！我的卡车被卡住了！我该怎么办？”  \n",
      "一只小鸟听到了卡车司机的话。小鸟说：“别担心，我可以帮你！”小鸟飞走了，带着它的朋友们回来。他们齐心协力搬开事故现场。  \n",
      "卡车司机非常高兴。他说：“谢谢你们，小鸟们！你们救了我的卡车！”小鸟们说：“不客气！我们很高兴能帮上忙！”于是他们都成了好朋友。  \n",
      "从那天起，卡车司机和小鸟们就开始互相帮助了。他们展示了即使是最小的朋友在一起合作时也能变得强大。每天早晨，鸟儿们都会用快乐的歌声叫醒卡车司机。\n"
     ]
    }
   ],
   "source": [
    "data_index = 87\n",
    "text = llm.generate(\n",
    "    prompt=prompt_style.apply(prompt=sft_data[data_index][\"instruction\"]),\n",
    "    max_new_tokens=300,\n",
    "    temperature=0.2,\n",
    "    top_p=0.8,\n",
    "    top_k=50,\n",
    ")\n",
    "print(sft_data[data_index][\"instruction\"])\n",
    "print(\"-\" * 100)\n",
    "print(text)\n",
    "print(\"-\" * 100)\n",
    "print(sft_data[data_index][\"output\"])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 62,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "从前，有两个好朋友，名叫汤姆和杰瑞。汤姆和杰瑞是最好的朋友，总是一起做所有事情。一天，汤姆和杰瑞在花园里玩耍。汤姆说：“我们来做点什么吧！”杰瑞说：“好啊，我们做什么？”  \n",
      "汤姆说：“我们来做个桃子吧！”于是，他们找了一些树枝、叶子和花，开始做桃子。汤姆和杰瑞用锤子、锯子和钉子把桃子捣碎。  \n",
      "当桃子做好时，汤姆和杰瑞都特别开心。他们各自拿起一块桃子，咬了一大口。桃子甜得可口了！他们都笑着说：“好好吃！”  \n",
      "汤姆和杰瑞又做了更多的桃子，并和花园里的其他动物分享。大家都特别开心，享受着美味的桃子。  \n",
      "故事结束了。\n",
      "----------------------------------------------------------------------------------------------------\n",
      "从前，有一个小女孩叫莉莉。她喜欢在草地上玩耍。一天，莉莉在玩耍时看到她的铲子在草地上滚动。她捡起铲子，开始在泥土里挖掘。  \n",
      "突然，莉莉的妈妈叫她进屋吃午饭。“莉莉，进来！”她说。“我铲子在草地里玩得可开心了！”  \n",
      "莉莉回答：“好的，妈妈！”她进屋吃了三明治。她的妈妈问：“你铲子在草地里玩得开心吗？”  \n",
      "“是的，妈妈！太好玩了！”莉莉笑着说。\n",
      "----------------------------------------------------------------------------------------------------\n",
      "从前，有一个小女孩叫莉莉。她喜欢在阳光下玩耍。一天，她在花园里看到了一只蝴蝶。它真漂亮！莉莉想抓住它，但它飞走了。  \n",
      "那天晚些时候，莉莉的妈妈让她帮忙洗衣服。莉莉看到一堆衣服，决定把它们整理好。她把所有的衬衫放在一堆，把裤子放在另一堆。  \n",
      "当天稍晚，莉莉的妈妈让她帮忙洗衣服。她给莉莉展示了一个大篮子，让她把所有的衣服放进去。篮子又大又重，但莉莉很强壮。她把所有的衣服都放了进去，感到很自豪。  \n",
      "那天晚上，莉莉上床睡觉，梦到了那只蝴蝶和那堆衣服。她迫不及待想再帮妈妈洗衣服。\n",
      "----------------------------------------------------------------------------------------------------\n",
      "从前，有一只小蚂蚁。蚂蚁非常饿，想找点食物。蚂蚁看到一个大苹果，爬了过去。但是，苹果上有个大洞，蚂蚁掉进了洞里。蚂蚁吓坏了，不知道该怎么办。突然，一只善良的蜜蜂看见了蚂蚁，飞下来帮助。蜜蜂说：“别担心，小蚂蚁，我会帮你的。”蜜蜂飞走了，带回来一些蜂蜜。蜜蜂把蜂蜜涂在了蚂蚁的脚上，蚂蚁感觉好多了。蚂蚁非常高兴，感谢蜜蜂。从那天起，蚂蚁和蜜蜂成了好朋友，一起经历了很多冒险。\n",
      "----------------------------------------------------------------------------------------------------\n",
      "从前，有一个小女孩叫莉莉。她喜欢在外面玩耍和采花。一天，她发现了一朵美丽的花，红得像红色的蝴蝶一样。她摘下花，把它放进篮子里。  \n",
      "当她走回家的路上，她看到天空中飘着几朵白云。云朵看起来像是要掉下来！莉莉觉得它们看起来像小兔子。她向兔子们挥手，它们也朝她挥手。  \n",
      "回到家后，她把花展示给妈妈，并告诉她关于那朵飘在空中的云。妈妈微笑着说：“那是一朵漂亮的云，莉莉。”莉莉感到很开心，也很爱。她把花插在一个花瓶里，看着它。她心里想着那朵兔子和那朵云的冒险，但她知道自己是安全的。\n",
      "----------------------------------------------------------------------------------------------------\n"
     ]
    }
   ],
   "source": [
    "test_cases = [\n",
    "    {\n",
    "        \"instruction\": \"词汇：铲子，草地\\n\",\n",
    "    },\n",
    "    {\n",
    "        \"instruction\": \"特点：转折\\n\",\n",
    "    },\n",
    "    {\n",
    "        \"instruction\": \"摘要：一只小蚂蚁在花园里寻找吃的，最后找到了一个苹果。\\n\",\n",
    "    },\n",
    "    {\n",
    "        \"instruction\": \"随机句子：天空中飘着几朵白云。\\n\",\n",
    "    },\n",
    "]\n",
    "prompts = [prompt_style.apply(prompt=case[\"instruction\"]) for case in test_cases]\n",
    "prompts.insert(0, \"Instruction: 汤姆和杰瑞是一对好朋友\\n\\nOutput:\")\n",
    "for prompt in prompts:\n",
    "    text = llm.generate(\n",
    "        prompt=prompt,\n",
    "        max_new_tokens=300,\n",
    "        temperature=0.2,\n",
    "        top_p=0.9,\n",
    "        top_k=30,\n",
    "    )\n",
    "    print(text)\n",
    "    print(\"-\" * 100)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "可以看到对简单的约束的支持意外地还是不错的：\n",
    "\n",
    "**关键词**能完全命中，**转折**虽然很**生硬**，但是看得出来理解了要加入转折。\n",
    "\n",
    "根据**摘要**生成也比较准确，**随机句子**方面没有办法完全包含原句，但是大差不差（感觉完全包含还是有点难为这个尺寸的模型了）。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 组合约束\n",
    "再来看看组合约束的效果👇："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "从前，有一个聪明的女孩叫露西。她喜欢在草地上玩耍。每天，她都会在草地上跑、跳、穿梭。  \n",
      "一天，露西在草地上看到一把锋利的铲子。她捡起来开始玩。她像船一样在地上划过来。真是太好玩了！  \n",
      "露西的朋友汤姆来找她玩。他看到她在玩铲子，也想来试试。他们都假装在草地上划过来。他们笑着玩得特别开心。  \n",
      "突然，露西踩到了一个坎子，铲子飞了起来！汤姆和露西都很惊讶，他们不知道那把铲子怎么会到那里的。他们在草地上飞快地跑着，铲子帮助他们找到更多的故事来探索。  \n",
      "总结一下：露西在草地上发现了一把锹，她在草地上用它当作船，她和她的朋友汤姐姐姐在一起玩耍。\n",
      "----------------------------------------------------------------------------------------------------\n",
      "从前有一只小蚂蚁，它是周围最小的蚂蚁。每天，这只小蚂蚁都在寻找食物。一天，它突然想到一个主意。它决定好好铺一块碗，里面放着一些食物，让其他蚂蚁也能吃。  \n",
      "小蚂蚁小心翼翼地把食物倒入碗中。然后，它开始四处张望。它看到了许多美味的苹果片，真是太甜了！它忍不住咬了一口苹果，这是它吃过的最好吃的苹果。  \n",
      "小蚂蚁非常高兴，开始带着碗回家。在路上，它注意到附近有几只蚂蚁。每只蚂蚁都有自己吃的食物。小蚂蚁真自豪华，心里满是这么多食物。  \n",
      "当小蚂蚁回到家时，它决定好好地把食物放在自己的碗里。这样，它就可以和其他蚂蚂蚁一起分享。现在，所有的蚂蚁都能吃到食物，大家都很开心。\n",
      "----------------------------------------------------------------------------------------------------\n",
      "从前，有一只小蚂蚁住在花园里。蚂蚁喜欢花园里的草地。一天，蚂蚁在花园里散步时，看见了一棵大树。树上结满了苹果。蚂蚁非常高兴，爬上了树。  \n",
      "当蚂蚁到达树顶时，它看到一个苹果。那是一个鲜艳的苹果，看起来真好吃。但就在他准备咬一口时，他听到了一声响亮的声音。那是树枝上的一只声音。树枝枝枝折断了，蚂蚁掉到了地上。  \n",
      "蚂蚁很害怕，但它没事。它爬回了家，吃了其他食物。结束。\n",
      "----------------------------------------------------------------------------------------------------\n"
     ]
    }
   ],
   "source": [
    "test_cases = [\n",
    "    {\n",
    "        \"instruction\": \"词汇：铲子，草地\\n特点：转折\\n\",\n",
    "    },\n",
    "    {\n",
    "        \"instruction\": \"词汇：铲子，草地\\n摘要：一只小蚂蚁在花园里寻找吃的，最后找到了一个苹果。\\n\",\n",
    "    },\n",
    "    {\n",
    "        \"instruction\": \"词汇：铲子，草地\\n随机句子：天空中飘着几朵白云。\\n摘要：一只小蚂蚁在花园里寻找吃的，最后找到了一个苹果。\\n\",\n",
    "    },\n",
    "]\n",
    "prompts = [prompt_style.apply(prompt=case[\"instruction\"]) for case in test_cases]\n",
    "for prompt in prompts:\n",
    "    text = llm.generate(\n",
    "        prompt=prompt,\n",
    "        max_new_tokens=300,\n",
    "        temperature=0.9,\n",
    "        top_p=1,\n",
    "        top_k=30,\n",
    "    )\n",
    "    print(text)\n",
    "    print(\"-\" * 100)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "混合约束的难度明显上升了，虽然看得出模型在努力地理解指令，但是结果并不理想。\n",
    "\n",
    "一方面可能我的`base`模型训练得可能还不够充分，另一方面`SFT`数据量少了。\n",
    "\n",
    "对于`SFT`之后模型生成的连贯性和逻辑性出现明显下降的问题，简单地检索了一下，一个可能的优化方法是在`SFT`数据里加入一些`pretrain`里的数据，这种做法称为`Replay`。\n",
    "\n",
    "时间有限还没来得及尝试，等后面有时间了可以试试，在之后的更新里同步分享结果给大家吧。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## LORA微调⌛\n",
    "\n",
    "我也尝试了在`SFT`数据上用`LORA`微调，发现效果并不好，loss下降得很慢，且远高于`SFT`全量微调的loss。\n",
    "\n",
    "![image4](https://erxuanyi-1257355350.cos.ap-beijing.myqcloud.com/image4.png)\n",
    "\n",
    "如上图，黄色🟡的是全量微调的loss，红色🔴的是`LORA`微调的loss，这里虽然只有两条，但实际上我尝试了不少`learning rate`和其他参数的组合，但结果都差不多。\n",
    "\n",
    "我猜测是因为模型太小了，用`Lora`微调时候使用较小的`r`和`alpha`，可训练参数量就更小，所以效果不好。\n",
    "\n",
    "于是我试了下将`Lora`的`r`和`alpha`调大（🟠从`8_16`调到`256_512`），发现效果好了不少，loss下降得更快了，但收敛速度还是要**远远慢于**全量微调。\n",
    "\n",
    "这时候的可训练参数量级已经接近`22M`，正好是模型自身的一半了，效果变好也是理所当然的，但这样显然已经失去了`LORA`微调的意义。\n",
    "\n",
    "> 关于`Lora`的正常使用，后面等有机会训练一个更大的`base model`的时候再尝试吧。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 小结\n",
    "1. 分享了`SFT`全量微调的一些实验结果\n",
    "2. 测试了一下`SFT`全量微调之后的指令遵循效果\n",
    "3. 分享了用`LORA`微调的一些实验结果"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "bigmodel",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
